package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.CACHE_PENETRATION_NULL_VALUE;

@Component
@Slf4j
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 开启线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 获取互斥锁  setNx  ----- setIfAbsent
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",RedisConstants.LOCK_SHOP_TTL,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放互斥锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 构造方法
     * @param stringRedisTemplate
     */
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意Java对象序列化为json并存储在String类型的key中，并且可以设置过期时间
     * @param key
     * @param value 任意java对象
     * @param time 过期时间
     * @param unit 过期时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        // 序列化
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 将任意java对象序列化成json并存储在String类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
     * @param key
     * @param value 任意java对象
     * @param time  逻辑过期时间
     * @param unit  逻辑过期时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        // 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        // 利用uint的 toSeconds 转换成Second
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis【逻辑过期】
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }


    /**
     * 根据指定的key查询缓存，并反序列为指定的类型，利用缓存空值来解决缓存穿透问题
     * @param keyPrefix  key前缀
     * @param id         查询数据库id
     * @param type       返回值类型
     * @param dbFallback 数据库查询方法
     * @param time       过期时间
     * @param unit       过期时间单位
     * @return
     * @param <R>   返回值类型
     * @param <ID>   查询数据库id类型
     */
    public <R,ID> R  queryWithPassThrough(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID,R> dbFallback,
            Long time,
            TimeUnit unit
    ) {
        String key = keyPrefix + id;
        //1. 查询Redis
        String json = stringRedisTemplate.opsForValue().get(key);
        //2. 判断是否存在
        if(StrUtil.isNotBlank(json)){
            //3. 存在，直接返回
            // 反序列化回 type 类型
            return JSONUtil.toBean(json,type);
        }
        //判断是否为空值
        if(json != null){
            // 返回
            return null;
        }
        //4. 不存在，根据id查询数据库
        R r = dbFallback.apply(id);
        //5. 不存在，返回错误
        if(r == null){
            // 写入空值
            stringRedisTemplate.opsForValue().set(key,CACHE_PENETRATION_NULL_VALUE,CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //6. 存在，写入Redis
        this.set(key,r,time,unit);
        //7. 返回
        return r;
    }


    /**
     * 根据指定的key查询缓存，并反序列成指定类型，利用逻辑过期解决缓存击穿问题
     * @param KeyPrefix   key前缀
     * @param id          查询数据库id
     * @param type        返回值类型
     * @param dbFallback  数据库查询方法
     * @param time        逻辑过期时间
     * @param unit        逻辑过期时间单位
     * @return
     * @param <R>         返回值类型
     * @param <ID>        查询数据库id类型
     */
    public <R,ID> R queryWithLogicalExpire(
            String KeyPrefix,
            ID id,
            Class<R> type,
            Function<ID,R> dbFallback,
            Long time,
            TimeUnit unit
    ) {
        // 查询Redis
        String json =  stringRedisTemplate.opsForValue().get(KeyPrefix + id);
        //1. 判断Redis中是否存在数据
        if(StrUtil.isBlank(json)){
            //1.1  不存在直接结束
            return null;
        }
        //2. 命中了，判断缓存是否过期
        //2.1 将JSON字符串反序列为对象
        RedisData redisData = JSONUtil.toBean(json,RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data,type);
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){ // 未过期
            // 直接返回
            return r;

        }
        // 3. 已过期，需要缓存重建
        // 4. 尝试获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //5. 获取互斥锁成功，开启独立线程，查询数据库，重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    //  重建缓存
                    //1. 查数据库
                    R r1 = dbFallback.apply(id);
                    //2. 写redis
                    this.setWithLogicalExpire(KeyPrefix,r1,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 6. 释放互斥锁
                    unLock(lockKey);
                }
            });
        }
        return r;
    }
}
